/*
 * Copyright 2010 WANdisco, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <apr_pools.h>

#include "svn_pools.h"
#include "svn_error.h"
#include "svn_fs.h"
#include "svn_repos.h"
#include "svn_path.h"
#include "svn_io.h"
#include "svn_delta.h"
#include "svn_config.h"
#include "svn_props.h"
#include "svn_base64.h"

#include "jniutil.h"

#include "com_googlecode_svnj_EditorDriver.h"

typedef struct method_holder {
  jmethodID *mid;
  const char *name;
  const char *sig;
} method_holder;


typedef struct edit_baton
{
  /* set by CTOR */
  const char *repo_dir;
  apr_pool_t *pool;
  jobject jeditor;
  svn_revnum_t end_rev;
  svn_fs_root_t *rev_root;
  /* so we can call back to java */
  JNIEnv *env;
  /* handle to the report we're running */
  void *report_baton;
} edit_baton;

/* pre declare */
static svn_error_t *
init_editor(const svn_delta_editor_t **editor,
            apr_pool_t *pool);

/* IDeltaEditor methods */
static jmethodID
  mid_setTargetRevision,
  mid_openRoot,
  mid_deleteEntry,
  mid_addDirectory,
  mid_openDirectory,
  mid_changeDirProp,
  mid_closeDirectory,
  mid_absentDirectory,
  mid_addFile,
  mid_openFile,
  mid_beginTextDelta,
  mid_appendTextDelta,
  mid_endTextDelta,
  mid_changeFileProp,
  mid_closeFile,
  mid_absentFile,
  mid_closeEdit,
  mid_abortEdit;

/* TxDeltaWindow methods */
/*static jmethodID
txmid_createWindow, txmid_addData, txmid_addDeltaOp;
*/
/* TxDeltaWindow jclass */
/*
static jclass txwindow_clazz;
*/
/* EditorDriver fields */
static jfieldID fid_editor, fid_ptr;

/* http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/types.html#wp16432 */
static method_holder methods[] = {
  { &mid_setTargetRevision, "setTargetRevision", "(J)V" },
  { &mid_openRoot,  "openRoot", "(J)V" },
  { &mid_deleteEntry, "deleteEntry", "(Ljava/lang/String;J)V" },
  { &mid_addDirectory, "addDirectory", "(Ljava/lang/String;Ljava/lang/String;J)V" },
  { &mid_openDirectory, "openDirectory", "(Ljava/lang/String;J)V" },
  { &mid_changeDirProp, "changeDirProp", "(Ljava/lang/String;Ljava/lang/String;)V" },
  { &mid_closeDirectory, "closeDirectory", "()V" },
  { &mid_absentDirectory, "absentDirectory", "(Ljava/lang/String;)V" },
  { &mid_addFile, "addFile", "(Ljava/lang/String;Ljava/lang/String;J)V" },
  { &mid_openFile, "openFile", "(Ljava/lang/String;J)V" },
  { &mid_beginTextDelta, "beginTextDelta", "(Ljava/lang/String;)V" },
  { &mid_appendTextDelta, "appendTextDelta", "(Ljava/lang/String;)V" },
  { &mid_endTextDelta, "endTextDelta", "()V" },
  { &mid_changeFileProp, "changeFileProp", "(Ljava/lang/String;Ljava/lang/String;)V" },
  { &mid_closeFile, "closeFile", "(Ljava/lang/String;)V" },
  { &mid_absentFile, "absentFile", "(Ljava/lang/String;)V" },
  { &mid_closeEdit, "closeEdit", "()V" },
  { &mid_abortEdit, "abortEdit", "()V" },
  { NULL, NULL, NULL }
};

static apr_pool_t *master_pool;

JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_initFields(JNIEnv *env, jclass clazz) {
  jclass eclass;
  method_holder *mh;
  eclass = (*env)->FindClass(env, SVNJPKG "/IDeltaEditor");
  if (eclass == NULL) {
    /* just return; VM will raise NoClassDefFoundError */
    return;
  }
  mh = methods;
  while (mh && mh->mid) {
    *(mh->mid) = (*env)->GetMethodID(env, eclass, mh->name, mh->sig);
    if (*(mh->mid) == NULL) {
      /*printf("CANNOT FIND %s\n", mh->name);*/
      /* just return; VM will raise NoSuchMethodError */
      return;
    }
    mh++;
  }

  /* return if field ID's not found; NoSuchFieldError raised */
  fid_editor = (*env)->GetFieldID(env, clazz, "editor",
                                  "L" SVNJPKG "/IDeltaEditor;");
  if (!fid_editor) return;
  fid_ptr    = (*env)->GetFieldID(env, clazz, "ptr", "J");
  if (!fid_ptr) return;

  /* TxDeltaWindow methods */
  /*
  txwindow_clazz = (*env)->FindClass(env, SVNJPKG "/TxDeltaWindow");
  if (txwindow_clazz == NULL) return;
  txwindow_clazz = (jclass)(*env)->NewGlobalRef(env, txwindow_clazz);
  txmid_addDeltaOp = (*env)->GetMethodID(env, txwindow_clazz, "addDeltaOp", "(III)V");
  if (!txmid_addDeltaOp) return;
  txmid_addData = (*env)->GetMethodID(env, txwindow_clazz, "addData", "([B)V");
  if (!txmid_addData) return;
  txmid_createWindow =
    (*env)->GetStaticMethodID(env, txwindow_clazz, "createWindow",
                              "(JJJII)L" SVNJPKG "/TxDeltaWindow;");
  if (!txmid_createWindow) return;
  */
  /* intialize pool */

  apr_initialize();
  master_pool = svn_pool_create(NULL);

}

JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_initSelf(JNIEnv *env, jobject self, jlong endRev, jstring jreposDir) {
  apr_pool_t *pool;
  edit_baton *eb;

  pool = svn_pool_create(master_pool);
  eb = apr_pcalloc(pool, sizeof(edit_baton));
  eb->pool = pool;

  eb->repo_dir = make_cstring(env, jreposDir, pool);
  eb->jeditor = (*env)->GetObjectField(env, self, fid_editor);
  /* java editor object needs to be a global ref, since we
   * hold onto this* reference for a long time in the editor
   * baton
   */
  eb->jeditor = (*env)->NewGlobalRef(env, eb->jeditor);
  eb->end_rev = (svn_revnum_t) endRev;
  /* store pointer to baton */
  {
    jlong ptr = (jlong)eb;
    (*env)->SetLongField(env, self, fid_ptr, ptr);
  }
}

static svn_error_t *
begin_report(edit_baton *eb, const char *fs_base, const char *target,
             const char* tgt_path, svn_boolean_t text_deltas,
             svn_depth_t depth, svn_boolean_t ignore_ancestry,
             svn_boolean_t send_copyfrom_args) {
  svn_repos_t *repo;
  const svn_delta_editor_t *editor;
  svn_fs_t *fs;
  /* report stuff */
  void *report_baton;

  SVN_ERR(init_editor(&editor, eb->pool));
  SVN_ERR(svn_repos_open(&repo, eb->repo_dir, eb->pool));
  /* initialize */
  fs = svn_repos_fs(repo);
  SVN_ERR(svn_fs_revision_root(&(eb->rev_root), fs, eb->end_rev, eb->pool));
  /* report */

  SVN_ERR(
    svn_repos_begin_report2(
      &report_baton,
      eb->end_rev,
      repo,
      fs_base, /* <src-path>, required not-null */
      target, /* <target>, default="" */
      tgt_path,/* <dst-path>, default=NULL */
      text_deltas,
      depth,
      ignore_ancestry,
      send_copyfrom_args,
      editor,
      eb,
      NULL, /* authz_read_func */
      NULL, /* authz_read_baton */
      eb->pool)
    );
  eb->report_baton = report_baton;
  return SVN_NO_ERROR;
  return SVN_NO_ERROR;
}

JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_beginReport
(JNIEnv *env, jobject self, jstring jfsBase, jstring jtarget,
 jstring jtgt_path, jboolean text_deltas, jint depth, jboolean ignore_ancestry,
 jboolean send_copyfrom_args) {

  edit_baton *eb;
  svn_error_t *err;
  apr_pool_t *pool;
  const char *fs_base, *target, *tgt_path;

  eb = (edit_baton*)(*env)->GetLongField(env, self, fid_ptr);
  eb->env = env;

  pool = eb->pool;
  fs_base = make_cstring(env, jfsBase, pool);
  target = make_cstring(env, jtarget, pool);
  tgt_path = make_cstring(env, jtgt_path, pool);
  
  err = begin_report(eb, fs_base, target, tgt_path, text_deltas,
                     depth, ignore_ancestry, send_copyfrom_args);
  if (err != SVN_NO_ERROR) {
    /* convert svn/apr errcode to java exception */
    throw_svn_exception(env, err, FALSE);
    /* clear existing svn error, for re-entrancy?
         - causes SEGV, in apr_pool_destroy()...
    */
    /*svn_error_clear(err);*/
  }
  return;
}

JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_addPath
(JNIEnv *env, jobject self, jstring jpath, jstring jlinkpath, jlong rev,
 jint depth, jboolean startEmpty, jstring jlocktoken) {
  edit_baton *eb;
  svn_error_t *err;
  const char *path, *linkpath, *locktoken;

  eb = (edit_baton*)(*env)->GetLongField(env, self, fid_ptr);
  path = make_cstring(env, jpath, eb->pool);
  linkpath = make_cstring(env, jlinkpath, eb->pool);
  locktoken = make_cstring(env, jlocktoken, eb->pool);

  if (linkpath == NULL) {
    err = svn_repos_set_path3(eb->report_baton,
                              path, 
                              rev,
                              depth,
                              startEmpty,
                              locktoken,
                              eb->pool);
  } else {
    err = svn_repos_link_path3(eb->report_baton,
                               path,
                               linkpath,
                               rev,
                               depth,
                               startEmpty,
                               locktoken,
                               eb->pool);
  }
  if (err != SVN_NO_ERROR) {
    /* convert svn/apr errcode to java exception */
    throw_svn_exception(env, err, FALSE);
  }

}


JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_startEdit(JNIEnv *env, jobject self) {
  edit_baton *eb;
  svn_error_t *err;
  eb = (edit_baton*)(*env)->GetLongField(env, self, fid_ptr);
  
  /* make sure editor has latest environment, it calls
   * back into java a lot!
   */
  eb->env = env;
  
  err = svn_repos_finish_report(eb->report_baton, eb->pool);
  if (err != SVN_NO_ERROR) {
    /* convert svn/apr errcode to java exception */
    throw_svn_exception(env, err, FALSE);
    /* FIXME: clear existing svn error, for re-entrancy?
     * but it causes SEGV - is it ok to leave error outstanding here?
     */
    /*svn_error_clear(err);*/
  }
  return;
}

JNIEXPORT void JNICALL
Java_com_googlecode_svnj_EditorDriver_cleanupSelf(JNIEnv *env, jobject self) {
  struct edit_baton *eb;
  jlong ptr = (*env)->GetLongField(env, self, fid_ptr);
  if (ptr) {
    eb = (struct edit_baton *)ptr;
    (*env)->DeleteGlobalRef(env,eb->jeditor);
    /* ### FIXME: cleanup txn here?  abort_edit() seems always
     * to be called regardless
     */
    svn_pool_destroy(eb->pool);
    (*env)->SetLongField(env,self,fid_ptr,(jlong)0);
  }
}

static svn_error_t *
svnj_target_revision(void *ebaton, svn_revnum_t rev, apr_pool_t *pool) {
  edit_baton *eb = ebaton;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_setTargetRevision, (jlong)rev);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_open_root(void *edit_baton,
               svn_revnum_t base_revision,
               apr_pool_t *dir_pool,
               void **root_baton)
{
  struct edit_baton *eb = edit_baton;
  JNIEnv *env = eb->env;
  *root_baton = eb;

  (*env)->CallVoidMethod(env, eb->jeditor, mid_openRoot, (jlong)base_revision);
  /* deliberate fault to test error propgation back to java
  if (TRUE) {
    return svn_error_create(SVN_ERR_FS_UNSUPPORTED_FORMAT,NULL,"hi there");
  } */
  SVNJ_JAVA_CHECK();
  /*return SVN_NO_ERROR;*/
}

static svn_error_t *
svnj_delete_entry(const char *path,
                  svn_revnum_t revision,
                  void *baton,
                  apr_pool_t *pool)
{
  edit_baton *eb = baton;
  JNIEnv *env = eb->env;

  jstring jpath = make_jstring(env, path);
  (*env)->CallVoidMethod(env, eb->jeditor,
                         mid_deleteEntry, jpath, (jlong)revision);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_add_directory(const char *path,
                   void *parent_baton,
                   const char *copyfrom_path,
                   svn_revnum_t copyfrom_revision,
                   apr_pool_t *dir_pool,
                   void **child_baton)
{
  struct edit_baton *eb = parent_baton;
  JNIEnv *env = eb->env;

  jstring jpath = NULL, jcopyfrom_path = NULL;
  jpath = make_jstring(env, path);
  if (copyfrom_path)  /* add with history */
    {
      jcopyfrom_path = make_jstring(env, copyfrom_path);
    }
  else  /* add without history */
    ;

  *child_baton = eb;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_addDirectory,
                         jpath, jcopyfrom_path,
                         (jlong)copyfrom_revision);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_open_directory(const char *path,
                    void *parent_baton,
                    svn_revnum_t base_revision,
                    apr_pool_t *dir_pool,
                    void **child_baton)
{
  struct edit_baton *eb =parent_baton;
  JNIEnv *env = eb->env;
  jstring jpath = make_jstring(env, path);

  *child_baton = eb;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_openDirectory,
                        jpath, (jlong)base_revision);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_change_dir_prop(void *parent_baton,
                     const char *name, const svn_string_t *value,
                     apr_pool_t *pool)
{
  struct edit_baton *eb = parent_baton;
  JNIEnv *env = eb->env;

  jstring jname = NULL, jvalue = NULL;
  jname = make_jstring(env, name);
  jvalue = make_jstring2(env, value);
  (*env)->CallVoidMethod(env, eb->jeditor, mid_changeDirProp,
                         jname, jvalue);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_close_dir(void *dir_baton, apr_pool_t *pool) {
  struct edit_baton *eb = dir_baton;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_closeDirectory);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_absent_dir(const char *path, void *parent_baton, apr_pool_t *pool) {
  struct edit_baton *eb = parent_baton;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_absentDirectory);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_add_file(const char *path,
              void *parent_baton,
              const char *copyfrom_path,
              svn_revnum_t copyfrom_revision,
              apr_pool_t *file_pool,
              void **file_baton)
{
  struct edit_baton *eb = parent_baton;
  jstring jpath = NULL, jcopyfrom_path = NULL;
  JNIEnv *env = eb->env;
  
  jpath = make_jstring(env, path);

  if (copyfrom_path)  /* add with history */
    {
      jcopyfrom_path = make_jstring(env, copyfrom_path);
    }
  else  /* add without history */
    ;
  *file_baton = eb;

  (*env)->CallVoidMethod(env, eb->jeditor, mid_addFile, jpath, jcopyfrom_path,
                         (jlong)copyfrom_revision);

  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_open_file(const char *path,
               void *parent_baton,
               svn_revnum_t base_revision,
               apr_pool_t *file_pool,
               void **file_baton)
{
  struct edit_baton *eb = parent_baton;
  JNIEnv *env = eb->env;
  jstring jpath = make_jstring(env, path);

  
  *file_baton = eb;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_openFile, jpath, (jlong)base_revision);
  SVNJ_JAVA_CHECK();
}

struct window_handler_baton
{
  edit_baton *eb;
  svn_boolean_t seen_first_window;  /* False until first window seen. */
  /* The _real_ window handler and baton. */
  svn_txdelta_window_handler_t handler;
  void *handler_baton;
};

/* stream handling */
static svn_error_t *
svnj_write_stream(void *baton, const char *buffer, apr_size_t *len) {
  svn_string_t str;
  jstring jstr;
  struct window_handler_baton *wb = baton;
  struct edit_baton *eb = wb->eb;
  JNIEnv *env = eb->env;

  str.data = buffer;
  str.len = *len;
  jstr = make_jstring2(env, &str);

  (*env)->CallVoidMethod(env, eb->jeditor, mid_appendTextDelta,
                         jstr);

  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_close_stream(void *baton) {
  struct window_handler_baton *wb = baton;
  struct edit_baton *eb = wb->eb;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_endTextDelta);

  SVNJ_JAVA_CHECK();
}

/* implements svn_txdelta_window_handler_t */
static svn_error_t *
svnj_window_handler(svn_txdelta_window_t *window,
                    void *baton)
{
  struct window_handler_baton *wb = baton;
  SVN_ERR(wb->handler(window, wb->handler_baton));
  if (!window) {
    /*printf("[handler2] end window\n");*/
  }
  return SVN_NO_ERROR;
}

static svn_error_t *
svnj_apply_textdelta(void *baton,
                     const char *base_checksum,
                     apr_pool_t *pool,
                     svn_txdelta_window_handler_t *handler,
                     void **handler_baton)
{
  struct edit_baton *eb = baton;
  JNIEnv *env = eb->env;
  struct window_handler_baton *wb;
  svn_stream_t *out_stream, *b64_stream;
  jstring jchecksum;

  /* init an output stream */
  out_stream = svn_stream_create(eb, pool);
  svn_stream_set_write(out_stream, svnj_write_stream);
  svn_stream_set_close(out_stream, svnj_close_stream);
  b64_stream = svn_base64_encode(out_stream, pool);
  /* init a window handler baton */
  wb = apr_palloc(pool, sizeof (struct window_handler_baton));
  svn_stream_set_baton(out_stream, wb);
 
  svn_txdelta_to_svndiff2(&(wb->handler), &(wb->handler_baton), b64_stream,
                          1, pool);
  *handler = svnj_window_handler;
  *handler_baton = wb;
  wb->eb = eb;
   jchecksum = make_jstring(env, base_checksum);
  (*env)->CallVoidMethod(env, eb->jeditor, mid_beginTextDelta,
                         jchecksum);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_change_file_prop(void *baton,
                      const char *name, const svn_string_t *value,
                      apr_pool_t *pool)
{
  struct edit_baton *eb = baton;
  JNIEnv *env = eb->env;
  jstring jname = NULL, jvalue = NULL;
  jname = make_jstring(env, name);
  jvalue = make_jstring2(env, value);
  (*env)->CallVoidMethod(env, eb->jeditor, mid_changeFileProp,
                         jname, jvalue);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_close_file(void *baton,
                const char *text_checksum, apr_pool_t *pool) {
  struct edit_baton *eb = baton;
  JNIEnv *env = eb->env;
  jstring jchecksum = make_jstring(env, text_checksum);
  (*env)->CallVoidMethod(env, eb->jeditor, mid_closeFile,
                         jchecksum);
  SVNJ_JAVA_CHECK();
}


static svn_error_t *
svnj_absent_file(const char *path, void *parent_baton,
                 apr_pool_t *pool) {
  struct edit_baton *eb = parent_baton;
  JNIEnv *env = eb->env;
  jstring jpath = make_jstring(env, path);
  (*env)->CallVoidMethod(env, eb->jeditor, mid_absentFile,jpath);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_close_edit(void *edit_baton, apr_pool_t *pool) {
  struct edit_baton *eb = edit_baton;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_closeEdit);
  SVNJ_JAVA_CHECK();
}

static svn_error_t *
svnj_abort_edit(void *edit_baton, apr_pool_t *pool) {
  struct edit_baton *eb = edit_baton;
  JNIEnv *env = eb->env;
  (*env)->CallVoidMethod(env, eb->jeditor, mid_abortEdit);
  SVNJ_JAVA_CHECK();
}


/*---------------------------------------------------------------*/

static svn_error_t *
init_editor(const svn_delta_editor_t **editor,
            apr_pool_t *pool)
{
  svn_delta_editor_t *my_editor;

  /* Set up the editor. */
  /*my_editor = svn_delta_default_editor(pool); */
  my_editor = apr_palloc(pool, sizeof(svn_delta_editor_t));
  memset(my_editor, 0, sizeof(svn_delta_editor_t));
  
  my_editor->set_target_revision = svnj_target_revision;
  my_editor->open_root           = svnj_open_root;
  my_editor->delete_entry        = svnj_delete_entry;
  my_editor->add_directory       = svnj_add_directory;
  my_editor->open_directory      = svnj_open_directory;
  my_editor->change_dir_prop     = svnj_change_dir_prop;
  my_editor->close_directory     = svnj_close_dir;
  my_editor->absent_directory    = svnj_absent_dir;
  my_editor->add_file            = svnj_add_file;
  my_editor->open_file           = svnj_open_file;
  my_editor->apply_textdelta     = svnj_apply_textdelta;
  my_editor->change_file_prop    = svnj_change_file_prop;
  my_editor->close_file          = svnj_close_file;
  my_editor->absent_file         = svnj_absent_file;
  my_editor->close_edit          = svnj_close_edit;
  my_editor->abort_edit          = svnj_abort_edit;

  *editor = my_editor;

  return SVN_NO_ERROR;
}
